/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;

namespace VisualEsse.DataEngine
{
    /// <summary>
    /// The ArrayStorage stores a matrix of doubles in a separate binary file 
    /// and makes an read/write access to any part of it.
    /// </summary>
    class ArrayStorage : IDisposable
    {
        private const string signature = "AS";
        private const int signatureLength = 2;

        /// <summary> Number of parts for width</summary>
        protected const int partsW = 6;
        /// <summary> Number of parts for height</summary>
        protected const int partsH = 3;

        /// <summary> Number of points for width</summary>
        protected int m;
        /// <summary> Number of points for height</summary>
        protected int n;

        private int partWidth;
        private int partHeight;
        private bool[,] presentedParts = new bool[partsW, partsH];

        private object[,] loadedParts = null;

        private const int preheaderSize = signatureLength + sizeof(int) * 4;
        private const int headerSize = preheaderSize + sizeof(bool) * partsW * partsH;
        private Stream stream;
        private BinaryReader reader;
        private BinaryWriter writer;

        public ArrayStorage(Stream stream, int width, int height)
        {
            m = width;
            n = height;
            this.stream = stream;

            partWidth = (int)(width / partsW);
            partHeight = (int)(height / partsH);

            //if (partWidth != (double)width / partsW ||
            //    partHeight != (double)height / partsH)
            //    throw new Exception("ArrayStorage: array must have proper lengths to be stored");

            LoadHeader();
        }

        /// <summary>
        /// Header: 
        ///   [Number of parts for W : int][Number of parts for H : int][Points for W: int][Points for H: int]
        ///   {[Part ## presented : bool] * partsW * partsH}
        /// </summary>
        private void LoadHeader()
        {
            BinaryReader reader = Reader;
            reader.BaseStream.Seek(0, SeekOrigin.Begin);
            presentedParts.Initialize();

            try
            {
                // Trying to read the header
                string psign = new string(reader.ReadChars(signature.Length));
                int pw = reader.ReadInt32();
                int ph = reader.ReadInt32();
                int pm = reader.ReadInt32();
                int pn = reader.ReadInt32();

                if (psign != signature || pw != partsW || ph != partsH || pm != m || pn != n)
                    throw new Exception();

                for (int i = 0; i < partsW; i++)
                    for (int j = 0; j < partsH; j++)
                    {
                        presentedParts[i, j] = reader.ReadBoolean();
                    }
            }
            catch
            {
                // If the header couldn't be read, then we create new one.
                BinaryWriter writer = Writer;
                writer.Seek(0, SeekOrigin.Begin);

                writer.Write(signature.ToCharArray());
                writer.Write(partsW);
                writer.Write(partsH);
                writer.Write(m);
                writer.Write(n);

                for (int i = 0; i < partsW; i++)
                    for (int j = 0; j < partsH; j++)
                    {
                        writer.Write(presentedParts[i, j] = false);
                    }
            }
        }

        /// <summary>
        /// Saves the part of the entire array to the storage.
        /// </summary>
        /// <param name="i">Start of this part in the entire array.</param>
        /// <param name="j">Start of this part in the entire array.</param>
        /// <param name="data">The part to be saved.</param>
        public void SaveData(int i, int j, double[,] data)
        {
            // Determing the first and the last parts that are required.
            int pI0, pJ0;
            int pIE, pJE;
            GetPartContains(i, j, out pI0, out pJ0);
            GetPartContains(i + data.GetLength(0) - 1, j + data.GetLength(1) - 1, out pIE, out pJE);

            // Copying the data.
            int srcI = 0, srcJ = 0;
            for (int pi = pI0; pi <= pIE; pi++)
            {
                for (int pj = pJ0; pj <= pJE; pj++)
                {
                    if (presentedParts[pi, pj]) // this part is presented already
                        continue;

                    if (pi == pI0 && ((double)i / partWidth != pI0))
                        continue;
                    if (pj == pJ0 && ((double)j / partHeight != pJ0))
                        continue;
                    if (pi == pIE && ((double)(i + data.GetLength(0)) / partWidth != pIE + 1))
                        continue;
                    if (pj == pJE && ((double)(j + data.GetLength(1)) / partHeight != pJE + 1))
                        continue;

                    SavePart(pi, pj, srcI, srcJ, data);
                    srcJ += partHeight;
                }

                srcJ = 0;
                srcI += partWidth;
            }
        }

        /// <summary>
        /// Returns a sub-array of the stored array.
        /// </summary>
        /// <param name="i">Start index for width of required part of an array.</param>
        /// <param name="j">Start index for height of required part of an array.</param>
        /// <param name="width">Width of required part of an array.</param>
        /// <param name="height">Height of required part of an array.</param>
        /// <returns>Returns a sub-array of the stored array.
        /// If there is any unpresented part then an exception will be thrown.</returns>
        public double[,] LoadData(int i, int j, int width, int height)
        {
            // Allocating memory.
            double[,] array = new double[width, height];
            double[,] source = new double[partWidth, partHeight];

            // Determing the first and the last parts that are required.
            int pI0, pJ0;
            int pIE, pJE;
            GetPartContains(i, j, out pI0, out pJ0);
            GetPartContains(i + width - 1, j + height - 1, out pIE, out pJE);

            // Copying the data.
            int destI = 0, destJ = 0, srcI = 0;
            for (int pi = pI0; pi <= pIE; pi++)
            {
                for (int pj = pJ0; pj <= pJE; pj++)
                {
                    if (!presentedParts[pi, pj])
                        throw new Exception("ArrayStorage: some parts of requested data are not presented in the storage");

                    // The copy process itself starts here!
                    LoadPart(pi, pj, source);

                    // Finding source array indices
                    srcI = 0;
                    int srcWidth = partWidth;
                    if (pi == pI0) srcI = i - pi * partWidth;
                    if (pi == pIE) srcWidth = i + width - pi * partWidth;

                    int srcJ = 0, srcHeight = partHeight;
                    if (pj == pJ0) srcJ = j - pj * partHeight;
                    if (pj == pJE) srcHeight = j + height - pj * partHeight;

                    for (; srcJ < srcHeight; srcJ++, destJ++)
                        for (int dI = destI, s = srcI; s < srcWidth; s++, dI++)
                            array[dI, destJ] = source[s, srcJ];
                }

                destJ = 0;
                destI += partWidth - srcI;
            }

            return array;
        }

        private void SavePart(int pi, int pj, int i, int j, double[,] data)
        {
            BinaryWriter writer = Writer;
            SeekToPart(pi, pj, writer.BaseStream);

            double[,] part = new double[partWidth, partHeight];

            for (int k = 0; k < partWidth; k++, i++)
                for (int p = 0, t = j; p < partHeight; p++, t++)
                {
                    part[k, p] = data[i, t];
                    writer.Write(data[i, t]);
                }

            presentedParts[pi, pj] = true;
            writer.Seek(preheaderSize + sizeof(bool) * (pi * partsH + pj), SeekOrigin.Begin);
            writer.Write(true);

            if (loadedParts == null)
                loadedParts = new object[partsW, partsH];
            loadedParts[pi, pj] = part;
        }

        private void LoadPart(int pi, int pj, double[,] destArray)
        {
            if (loadedParts != null && loadedParts[pi, pj] != null)
            {
                double[,] part = (double[,])loadedParts[pi, pj];
                for (int i = 0; i < partWidth; i++)
                    for (int j = 0; j < partHeight; j++)
                    {
                        destArray[i, j] = part[i, j];
                    }

                return;
            }

            BinaryReader reader = Reader;
            SeekToPart(pi, pj, reader.BaseStream);

            for (int i = 0; i < partWidth; i++)
                for (int j = 0; j < partHeight; j++)
                {
                    destArray[i, j] = reader.ReadDouble();
                }
        }

        private void SeekToPart(int pi, int pj, Stream s)
        {
            s.Seek(headerSize + sizeof(double) * (partHeight * partWidth * (partsH * pi + pj)),
                SeekOrigin.Begin);
        }

        private BinaryWriter Writer
        {
            get
            {
                if (writer == null)
                    writer = new BinaryWriter(stream);
                return writer;
            }
        }

        private BinaryReader Reader
        {
            get
            {
                if (reader == null) reader = new BinaryReader(stream);
                return reader;
            }
        }


        /// <summary>
        /// Gets the list of bounding boxes for parts of an array that are not presented in this storage.
        /// </summary>
        /// <param name="i">Start index for width of required part of an array.</param>
        /// <param name="j">Start index for height of required part of an array.</param>
        /// <param name="width">Width of the required part of an array.</param>
        /// <param name="height">Height of the required part of an array.</param>
        /// <returns>Gets the list of bounding boxes.
        /// If there is any unpresented part then returned value is null.</returns>
        public List<Rectangle> GetUnpresentedBoundingBoxes(int i, int j, int width, int height)
        {
            List<Rectangle> parts = null;
            int pI0, pJ0;
            int pIE, pJE;
            GetPartContains(i, j, out pI0, out pJ0);
            GetPartContains(i + width - 1, j + height - 1, out pIE, out pJE);

            for (int pi = pI0; pi <= pIE; pi++)
                for (int pj = pJ0; pj <= pJE; pj++)
                {
                    // We have found a part that is not presented in the file.
                    if (!presentedParts[pi, pj])
                    {
                        if (parts == null)
                            parts = new List<Rectangle>(partsH * partsW);
                        parts.Add(GetRectangleForPart(pi, pj));
                    }
                }

            return parts;
        }

        private Rectangle GetRectangleForPart(int pi, int pj)
        {
            return new Rectangle(
                pi * partWidth, pj * partHeight,
                partWidth, partHeight);
        }

        private void GetPartContains(int i, int j, out int partI, out int partJ)
        {
            partI = (int)Math.Floor((double)i / partWidth);
            partJ = (int)Math.Floor((double)j / partHeight);
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (stream != null)
            {
                stream.Dispose();
                stream = null;
            }
        }

        #endregion
    }
}
